Skip to main content
Notes

OSX API_AVAILABLE Macro

· 5 min read

There are various way one could use the preprocessing macros. Learning how to properly use them will end up in some bizarre codebases. If you have ever encountered the <os/availability.h> header file on macOS, you'll find some macros that are confusing to interpret. I stumbled upon such macro that I'd like to discuss.

The macro is defined as follows:

/*
* API Introductions
*
* Use to specify the release that a particular API became available.
*
* Platform names:
* macos, ios, tvos, watchos
*
* Examples:
* API_AVAILABLE(macos(10.10))
* API_AVAILABLE(macos(10.9), ios(10.0))
* API_AVAILABLE(macos(10.4), ios(8.0), watchos(2.0), tvos(10.0))
*/
#define API_AVAILABLE(...) \
__API_AVAILABLE_GET_MACRO(__VA_ARGS__,__API_AVAILABLE8, __API_AVAILABLE7, __API_AVAILABLE6, __API_AVAILABLE5, __API_AVAILABLE4, __API_AVAILABLE3, __API_AVAILABLE2, __API_AVAILABLE1, 0)(__VA_ARGS__)

At first glance, this seems terrifying. The documentation helps provide a bit of information as to how the macro should be invoked, but the implementation detail that I'm interested in is for one to self study. __API_AVAILABLE_GET_MACRO is another parameterized macro that is not defined in this file but in <AvailabilityInternal.h> header file. If we browse that file, we get the definition as:

#define __API_AVAILABLE_PLATFORM_macos(x) macos,introduced=x
#define __API_AVAILABLE_PLATFORM_macosx(x) macosx,introduced=x
#define __API_AVAILABLE_PLATFORM_ios(x) ios,introduced=x
#define __API_AVAILABLE_PLATFORM_watchos(x) watchos,introduced=x
#define __API_AVAILABLE_PLATFORM_tvos(x) tvos,introduced=x

#if defined(__has_attribute)
#if __has_attribute(availability)
#define __API_A(x) __attribute__((availability(__API_AVAILABLE_PLATFORM_##x)))
#else
#define __API_A(x)
#endif
#else
#define __API_A(x)
#endif

#define __API_AVAILABLE1(x) __API_A(x)
#define __API_AVAILABLE2(x,y) __API_A(x) __API_A(y)
#define __API_AVAILABLE3(x,y,z) __API_A(x) __API_A(y) __API_A(z)
#define __API_AVAILABLE4(x,y,z,t) __API_A(x) __API_A(y) __API_A(z) __API_A(t)
#define __API_AVAILABLE5(x,y,z,t,b) __API_A(x) __API_A(y) __API_A(z) __API_A(t) __API_A(b)
#define __API_AVAILABLE6(x,y,z,t,b,m) __API_A(x) __API_A(y) __API_A(z) __API_A(t) __API_A(b) __API_A(m)
#define __API_AVAILABLE7(x,y,z,t,b,m,d) __API_A(x) __API_A(y) __API_A(z) __API_A(t) __API_A(b) __API_A(m) __API_A(d)
#define __API_AVAILABLE8(x,y,z,t,b,m,d,l) __API_A(x) __API_A(y) __API_A(z) __API_A(t) __API_A(b) __API_A(m) __API_A(d) __API_A(l)
#define __API_AVAILABLE_GET_MACRO(_1,_2,_3,_4,_5,_6,_7,_8,NAME,...) NAME

The focal point of this blog is that how the expansion of API_AVAILABLE() takes place. Let's try to study one of the example. Say that this macro is invoked as: API_AVAILABLE(macos(10.10), ios(9.9)). The following expansion would take place:

/* invoking the macro */
API_AVAILABLE(macos(10.10), ios(9.9))

/* would expand to */
__API_AVAILABLE_GET_MACRO(macos(10.10), ios(9.9),__API_AVAILABLE8, __API_AVAILABLE7, __API_AVAILABLE6, __API_AVAILABLE5, __API_AVAILABLE4, __API_AVAILABLE3, __API_AVAILABLE2, __API_AVAILABLE1, 0)(macos(10.10), ios(9.9))

/*
* realize that the ninth argument to `__API_AVAILABLE_GET_MACRO` is the one that this macro expands to.
* after the ninth argument, this macro is defined to be variadic, and the expansion doesn't use them either.
* So the expansion would look like
*/
__API_AVAILABLE2(macos(10.10), ios(9.9))

/* which would further expand to */
__API_A(macos(10.10)) __API_A(ios(9.9))

/* which would then be expanded to (assuming that `__has_attribute` is defined and `availability` is available) */
__attribute__((availability(__API_AVAILABLE_PLATFORM_macos(10.10)))) __attribute__((availability(__API_AVAILABLE_PLATFORM_ios(9.9))))

/* finally, the expansion of `__API_AVAILABLE_PLATFORM_XXX()` macro will expand as */
__attribute__((availability(macos,introduced=10.10))) __attribute__((availability(ios,introduced=9.9)))

Here, the magic happens during the expansion of __API_AVAILABLE_GET_MACRO. Notice that it expanded to __API_AVAILABLE2() given that we called API_AVAILABLE with two arguments. This is possible because of how __VA_ARGS__ works. GNU's Variadic Macros states the following:

The variable argument is completely macro-expanded before it is inserted into the macro expansion, just like an ordinary argument.

Consider that we have the following macro definition:

#define args(...) \
printf("%s\n", #__VA_ARGS__)

int
main (void)
{
args(Hello, World!);

return (0);
}

After compilation, this program would output the following:

$ ./prog
Hello, World!

Inside the definition of args, the variadic arguments was converted to string using Stringification. Notice that the token comma (,) was also part of the variadic argument. GNU's documentation states the following:

[This] kind of macro is called variadic. When the macro is invoked, all the tokens in its argument list after the last named argument (this macro has none), including any commas, become the variable argument.

Because of this property, after the invokation and expansion of API_AVAILABLE, the variable arguments are replaced in this macro, thereby allowing the expansion of the subsequent macro to have the comma included and shifting the arguments as needed. Consider another example:

API_AVAILABLE(macos(5.5), ios(6.6), macosx(7.7), watchos(8.8))

The expansion would be as follows:

__API_AVAILABLE_GET_MACRO(macos(5.5), ios(6.6), macosx(7.7), watchos(8.8),__API_AVAILABLE8, __API_AVAILABLE7, __API_AVAILABLE6, __API_AVAILABLE5, __API_AVAILABLE4, __API_AVAILABLE3, __API_AVAILABLE2, __API_AVAILABLE1, 0)(macos(5.5), ios(6.6), macosx(7.7), watchos(8.8))

As mentioned previously, the variable argument is completely macro-expanded before the macro expansion. This will include the comma token that may be available in the variable argument. The expansion to __API_AVAILABLE_GET_MACRO contains multiple arguments, but the first 9 arguments are only acknowledged where the ninth argument is the only one that will be used. In this case, it would be __API_AVAILABLE4, which is the one we need as we supplied API_AVAILABLE with four arguments.

Also, it uses one of the good practice when using macros. Notice the following fragment:

#if defined(__has_attribute)
#if __has_attribute(availability)
#define __API_A(x) __attribute__((availability(__API_AVAILABLE_PLATFORM_##x)))
#else
#define __API_A(x)
#endif
#else
#define __API_A(x)
#endif

Using macro can be helpful but they come at a cost. Since they are evaluated by the preprocessor (cpp(1)), it sometime leads to bugs that can be hard to trace. Here, if we are working on a system that does not have the __has_attribute operator or the availability attribute, then we must ensure that the macro expands to nothing. If we did not do such check, we will end up with a compilation error.